﻿
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace DailyFx.Book.Repo
{
    using Common;

	[System.Flags]
	enum TrimmingOption
	{
		None		= 0,
		
		Leading		= 1,
		Trailing	= 2,
		
		Both		= Leading | Trailing
	}

    public class CsvReader : IDisposable
    {
		/// <summary>
		///		the default delimiter character separating each field.
		/// </summary>
		public const char DefaultDelimiter = ',';

		/// <summary>
		///		the default quote character wrapping every field.
		/// </summary>
		public const char DefaultQuote = '"';

		/// <summary>
		///		the default escape character letting insert quotation characters inside a quoted field.
		/// </summary>
		public const char DefaultEscape = '"';
		
		/// <summary>
		/// Defines the default comment character indicating that a line is commented out.
		/// </summary>
		public const char DefaultComment = '#';

		/// <summary>
		///		the default encoding of text .
		/// </summary>
		public static readonly Encoding DefaultEncoding = Encoding.UTF8;


		/// <summary>
		///		the default (hot) buffer size.
		/// </summary>
		public const int DefaultBufferSize = 0x1000;


		// Special Keys  .
		//
		public const char SPACE = ' ';
		public const char TAB = '\t';
		public const char CR = (char)0x0d;
		public const char LF = (char)0x0a;


		/// <summary>
		///		Contains the <see cref="T:TextReader"/> pointing to file stream .
		/// </summary>
		private readonly TextReader _readerImpl;


		/// <summary>
		///		Contains the delimiter character separating each field.
		/// </summary>
		private char _delimiter;

		/// <summary>
		///		Contains the quotation character wrapping every field.
		/// </summary>
		private char? _quote;

		/// <summary>
		///		Contains the comment character indicating that a line is commented out.
		/// </summary>
		private char _comment;

		/// <summary>
		///		Contains the escape character letting insert quotation characters inside a quoted field.
		/// </summary>
		private char _escape;

		/// <summary>
		///		Contains the special charactor mask for field parsing .
		/// </summary>
		private int _keyMask;

		/// <summary>
		///		Contains the actual special charactor mask when in or out a quoted field .
		/// </summary>
		private int _actualKeyMask;

		/// <summary>
		///		Determines how fields should be trimmed.
		/// </summary>
		private TrimmingOption _trimmingOption;

		/// <summary>
		///		Contains the field headers.
		/// </summary>
		// private CsvRow _header;

		/// <summary>
		///		Contains the buffered record for singular record reading .
		/// </summary>
		private readonly CsvRecord[] _row;

		/// <summary>
		///		Contains the metadata of the data sourse .
		/// </summary>
		private Schema _metadata;


		/// <summary>
		///		Contains the current record index, row number ??.
		/// </summary>
		private long _currentRecordNumber; // Row Number

		/// <summary>
		///		Contains the array of the field values for the current record.
		/// </summary>
		private readonly string[] _fields = new string [32];

		/// <summary>
		///		Contains the length in bytes of field being built.
		/// </summary>
		private int _currentFieldLength;

		/// <summary>
		///		Contains the starting position of the field being built .
		/// </summary>
		private int _currentFieldHead ;

		/// <summary>
		///		Contains the index of the next unread field.
		/// </summary>
		private int _nextFieldIndex;

		/// <summary>
		///		Contains the maximum number of fields to retrieve for each record.
		/// </summary>
		private int _fieldCount;

		/// <summary>
		///		Contains the hot buffer.
		/// </summary>
		private readonly char[] _buffer;

		/// <summary>
		///		Contains the current hot buffer length.
		/// </summary>
		private int _bufferLength;

		/// <summary>
		///		Contains the readed index of the buffer .
		/// </summary>
		private System.Int32 _bufferIndex;


		/// <summary>
		///		Indicates if the class is disposed .
		/// </summary>
        private bool disposed;



		/// <summary>
		///		c'tor .
		/// </summary>
        public CsvReader( TextReader reader )
        {
			// Debug.Assert ( reader != null, "Reader must never be null.");
            Check.NotNull ( reader, "reader" );

			this._readerImpl = reader;

			this._delimiter = CsvReader.DefaultDelimiter;
			this._quote = CsvReader.DefaultQuote;

			UpdateKeyMask();

            // used to parse singular records
            this._row = new CsvRecord [1];

			this._buffer = new char[ DefaultBufferSize ];
        }




		/// <summary>
		///		Gets the delimiter character separating each field.
		///		- By default,  the delimiter is a comma (<c>,</c>).
		/// </summary>
		public char Delimiter
		{
			get
			{
				return this._delimiter;
			}
			set
			{

				if ( value == this._quote )
				{
					throw new System.ArgumentException (
						"Fields ' Delimiter and Quote cannot be the same charactor.");
				}
				this._delimiter = value;
				this.UpdateKeyMask ();
			}
		}

		/// <summary>
		///		Gets the quotation character wrapping every field.
		/// </summary>
		/// <remarks>
		///		Note that fields are not required to be quoted . 
		///			However, quoting a field allows it to contain the delimiter character itself, 
		///			along with new line characters ( ie. a multi-line field ).
		/// </remarks>
		public char? Quote
		{
			get
			{
				return this._quote;
			}
			set
			{
				if (value == this._delimiter) 
				{
					throw new System.ArgumentException ( 
						"Fields' quote and delimiter cannot be the same charactor." );
				}

				this._quote = value;
				this.UpdateKeyMask();
			}
		}

      
		/// <summary>
		///		Gets the current record index, ( row number for non-multi-line ).
		/// </summary>
		public virtual long CurrentRecordNumber
		{
			get
			{
				return this._currentRecordNumber;
			}
		}


        /// <summary>
        ///		Indicates whether there are more records to read.
        /// </summary>
		public bool HasMoreRecords ()
		{
				if (this._bufferIndex < this._bufferLength)
				{
					// the buffer isn't empty so there must be more records
					return true;
				}
                // the buffer is empty, so we only have more records if we successfully fill it
				return this.RequireBuffer ();
		}

        /// <summary>
        ///		Reads the first record from the underlying stream data 
		///		and try assign it to <see cref="HeaderRecord"/>.
        /// </summary>
        public CsvRecord ReadHeader ()
        {
            if ( parseRecords ( this._row, 0, 1) == 1)
            {
                ++this._currentRecordNumber;
				
				// todo :
				return this._row[0];
            }

            return null;
        }

        /// <summary>
        ///		Reads a <see cref="CsvRecord"/> from the underlying stream data .
        /// </summary>
        /// <returns>
        public CsvRecord ReadRecord ()
        {
            if ( parseRecords(  this._row, 0, 1) == 1)
            {
                ++this._currentRecordNumber;
                return this._row[0];
            }

            return null;
        }

        /// <summary>
        ///		Reads at most <paramref name="count"/> <see cref="CsvRecord"/>s 
		///		and populates <paramref name="buffer"/> with them, beginning at index <paramref name="offset"/>.
        /// </summary>
        /// <remarks>
        ///		When reading a lot of data, it is possible that better performance 
		///		can be achieved by using this method.
        /// </remarks>
        public int ReadRecords ( CsvRecord[] buffer, int offset, int count )
        {
            var read = parseRecords ( buffer, offset, count);
            this._currentRecordNumber += read;
            return read;
        }

		
        /// <summary>
        ///		Attempts to skip a record in the data, 
		///		and increments <see cref="CurrentRecordNumber"/> if successful.
        /// </summary>
		public bool SkipRecord ( bool incRow = true )
        {
            return this.SkipRecords (1, incRow ) == 1;
        }

        /// <summary>
        ///		Attempts to skip <paramref name="count"/> records in the data, 
		///		and increments <see cref="CurrentRecordNumber"/> by the number of records actually skipped.
		///		
		///		If there are fewer than <paramref name="count"/> records remaining in the data, 
		///		this method will skip the remaining records and return the number of records actually skipped.
        /// </summary>
        public int SkipRecords ( int count, bool incRow = true )
        {
            var skipped = 0;
            var quoted = false;

            while ( skipped < count )
            {
                if ( HasMoreRecords() )	// PERFORMANCE NOTES:
										// For Common case( still more records), keeps no branch in IL
                {
                    while ( true )
                    {
                        if ( !this.IsBufferEmpty() )
                        {
                            var ch = this._buffer[this._bufferIndex++];

                            if ( !this.IsPossiblyKeychar(ch) )
                            {
                                // if it's definitely not a special character, 
								// then we can just continue on with the loop
                                continue;
                            }

                            if ( !quoted )
                            {
                                if ( ch == this._quote )
                                {
                                    quoted = true;
									// since we're in a quoted area, the only special character is the quote itself .
									this._actualKeyMask = this._quote.Value;
                                }
                                else if (ch == CsvReader.CR)
                                {
                                    // we need to look at the next character, so make sure it is available
                                    if (this.IsBufferEmpty() && !this.RequireBuffer() )
                                    {
                                        // last character available was CR, 
										// so we know we're done at this point
                                        break;
                                    }

                                    // we deal with CRLF right here by checking if the next character is LF, in which case we just discard it
                                    if ( this._buffer[this._bufferIndex] == CsvReader.LF )
                                    {
                                        ++this._bufferIndex;
                                    }
                                    break;
                                }
                                else if (ch == CsvReader.LF)
                                {
                                    break;
                                }
                            }
                            else if (ch == this._quote )
                            {
                                // we need to look at the next character, so make sure it is available
                                if ( this.IsBufferEmpty() && !this.RequireBuffer() )
                                {
                                    break;
                                }

								if (this._buffer[this._bufferIndex] == this._quote)
                                {
									// quote is escaped, so just swallow it
                                    ++this._bufferIndex;
                                }
                                else
                                {
									// quote isn't escaped, so we are no longer in a quoted area
                                    quoted = false;
                                    this._actualKeyMask = this._keyMask;
                                }
                            }
                        }
                        else if ( !this.RequireBuffer() )
                        {
                            // all out of data, so we successfully skipped the final record
                            break;
                        }
                    }

                    ++skipped;
                }
                else
                {
                    break;
                }
            }

            if ( incRow )
            {
                this._currentRecordNumber += skipped;
            }

            return skipped;
        }

		/// <summary>
		///		Attempts to parses text in stream into records .
		/// </summary>
		public int parseRecords ( CsvRecord[] rows, int offset, int count)
		{

			var ch = char.MinValue;
			var quoted = false;

			var nParsed = 0;

			for ( var i = offset; i < offset + count; ++i )
			{
				while (true)
				{
					if ( !this.IsBufferEmpty() )
					{
						ch = this._buffer[ this._bufferIndex++];

						if ( !this.IsPossiblyKeychar (ch) )
						{
							// if it's definitely not a special character, 
							// then we can just append it and continue on with the loop .
							ReadChar ( quoted);
							continue;
						}

						if ( !quoted )
						{
							if ( ch == this._delimiter )
							{
								this.ReadField () ;
							}
							else if (ch == this._quote)
							{
								IngoreChar ();
								quoted = true;

								// since we're in a quoted area, the only special character is the quote itself
								this._actualKeyMask = this._quote.Value;
							}
							else if (ch == CsvReader.CR)
							{
								// we need to look at the next character, so make sure it is available
								if ( this.IsBufferEmpty() && !this.RequireBuffer() )
								{
									// undelimited CR indicates the end of a record, so add the existing value and then exit
									this.ReadField();
									rows[i] = this.PopupRecord ();

									break;
								}

								// we deal with CRLF right here by checking if the next character is LF, in which case we just discard it
								if (this._buffer[this._bufferIndex] == CsvReader.LF)
								{
									++this._bufferIndex;
								}

								// undelimited CR or CRLF both indicate the end of a record, so add the existing value and then exit
								this.ReadField();
								rows[i] = this.PopupRecord();

								break;
							}
							else if (ch == CsvReader.LF)
							{
								// undelimited LF indicates the end of a record, so add the existing value and then exit
								this.ReadField();
								rows[i] = this.PopupRecord();

								break;
							}
							else
							{
								// it wasn't a special character after all, so just append it
								this.ReadChar( false);
							}
						}
						else if (ch == this._quote)
						{
							// we need to look at the next character, so make sure it is available
							if ( this.IsBufferEmpty() && !this.RequireBuffer() )
							{
								// out of data
								quoted = false;
								this._actualKeyMask = this._keyMask;

								this.ReadField();
								rows[i] = this.PopupRecord();

								break;
							}

							if (this._buffer[this._bufferIndex] == this._quote /*this._escape*/ )
							{
								// quote is escaped, so append it to the value and discard the escape character
								IngoreChar ();
								++this._bufferIndex;
								ReadChar( true );
							}
							else
							{
								// quote isn't escaped, so we are no longer in a delimited area
								IngoreChar ();
								quoted = false;
								this._actualKeyMask = this._keyMask;
							}
						}
						else
						{
							// it wasn't a special character after all, so just append it
							ReadChar (true);
						}
					}
					else if ( !this.RequireBuffer() )
					{
						if ( this._currentFieldLength > 0 )
						{
							// a value is outstanding, so add it .
							this.ReadField() ;
						}

						if (ch == this._delimiter)
						{
							// special case: last character is a separator, 
							// which means there should be an empty value after it. eg. "foo," results in ["foo", ""]
							this.ReadEmptyField();
							// this.ReadField();
							rows[i] = this.PopupRecord();
							break;
						}
						else
						{
							var record = this.PopupRecord() ;

							if (record != null)
							{
								rows[i] = record;
								++nParsed;
							}
						}

						// data exhausted - we're done, even though we may not have filled the records array
						return nParsed;
					}
				}

				++nParsed;
			}

			return nParsed;
		}

		/// <summary>
		///		Builds a data record by current fields buffer .
		/// </summary>
		private CsvRecord PopupRecord ( )
		{
			if ( this._nextFieldIndex == 0 )
			{
				return null;
			}

			var result = new string [ this._nextFieldIndex ];
			Array.Copy( this._fields, 0, result, 0, this._nextFieldIndex );

			// clear
			this._nextFieldIndex = 0;

			// cast is to select the internal constructor, which is faster
			return new CsvRecord ( (IList<string>)result );
		}

		/// <summary>
		///		Attempts the parse a field's value string .
		/// </summary>
		private void ReadField ()
		{
				var startIndex = this._currentFieldHead;
				var endIndex = startIndex + this._currentFieldLength ;

				if ( (this._trimmingOption & TrimmingOption.Leading) != TrimmingOption.None )
				{
					while ( startIndex < endIndex && IsWhitespace(_buffer[startIndex]) )
					{
						++startIndex;
					}
				}

				if ( (this._trimmingOption & TrimmingOption.Leading) != TrimmingOption.None )
				{
					while ( endIndex > startIndex && IsWhitespace( this._buffer[endIndex - 1]) )
					{
						--endIndex;
					}
				}

				// this._currentFieldHead = 0;
				this._currentFieldLength = 0;

			this._fields [ this._nextFieldIndex++ ] = new string( _buffer, startIndex, endIndex - startIndex);
		}

		/// <summary>
		///		Empty field .
		/// </summary>
		private void ReadEmptyField()
		{
			this._fields[this._nextFieldIndex++] = String.Empty;
		}


		public void ReadChar ( bool quoted)
		{
			if (this._currentFieldLength == 0 )
			{
				// this is the first included char (for this demarcation), 
				// so we need to set our buffer start index
				this._currentFieldHead = _bufferIndex - 1;
			}

			// the overall value length has increased, 
			// as well as the piece of the value within the parser's buffer
			++this._currentFieldLength;

		}

		private void IngoreChar ()
		{ 
			// Nothing ?
		}
	

        /// <summary>
        ///		Closes this <c>CsvReader</c>.
		///		- This method is an alternative means of disposing the <c>CsvReader</c> ,
		///		- Generally one should prefer a <c>using</c> block to automatically dispose of it .
        /// </summary>
        public void Close()
        {
            this.Dispose();
        }

		/// <summary>
		///		
		/// </summary>
        public void Dispose ()
        {
            this.Dispose2 ( true );
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose2 ( bool disposing )
        {
            if ( disposing )
            {
				// this._readerImpl = null;
                // this._readerImpl.Dispose();
            }
			this.disposed = true ;
        }

		/// <summary>
		///		Updates the mask used to quickly detect characters that are definitely not special 
		///		(effectively a bloom filter, where false positives are possible but not false negatives)
		/// </summary>
		private void UpdateKeyMask ()
        {
            this._keyMask = this._delimiter | this._quote.GetValueOrDefault() | 
										CsvReader.CR | CsvReader.LF;

            this._actualKeyMask = this._keyMask;
        }

        /// <summary>
		///		Checks whether the character is possibly a special character
        /// </summary>
		private bool IsPossiblyKeychar ( char ch )
        {
            return ( ch & this._actualKeyMask) == ch;
        }

		/// <summary>
		///		
		/// </summary>
		private bool IsWhitespace ( char ch )
		{
			return ch == SPACE || ch == TAB;
		}

		/// <summary>
		///		Requires new buffer bytes from the data stream .
		/// </summary>
		private bool RequireBuffer ()
		{
			int i =  DefaultBufferSize - _currentFieldLength;
			Array.Copy(_buffer, i, _buffer, 0, _currentFieldLength);

			int n = this._readerImpl.Read (this._buffer, _currentFieldLength, i );
			this._bufferLength = n + _currentFieldLength;

			this._bufferIndex = _currentFieldLength ;
			this._currentFieldHead = 0;

			return n > 0;
		}

		private bool IsBufferEmpty ()
		{
			return this._bufferIndex == this._bufferLength;
		}

		
    }


}